package org.sdnstproject.interlayer.interlayerProxy;

import com.google.common.primitives.Longs;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.Unpooled;

import org.sdnstproject.interlayer.OF13Protocal.FuBarrierReply;
import org.sdnstproject.interlayer.OF13Protocal.FuEchoReply;
import org.sdnstproject.interlayer.OF13Protocal.FuFeaturesReply;
import org.sdnstproject.interlayer.OF13Protocal.FuGetConfigReply;
import org.sdnstproject.interlayer.OF13Protocal.FuHello;
import org.sdnstproject.interlayer.OF13Protocal.FuRoleReply;
import org.sdnstproject.interlayer.utils.Utils;
import org.projectfloodlight.openflow.exceptions.OFParseError;
import org.projectfloodlight.openflow.protocol.*;
import org.projectfloodlight.openflow.protocol.actionid.OFActionId;
import org.projectfloodlight.openflow.protocol.errormsg.OFErrorMsgs;
import org.projectfloodlight.openflow.protocol.instruction.OFInstruction;
import org.projectfloodlight.openflow.protocol.instructionid.OFInstructionId;
import org.projectfloodlight.openflow.types.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

public class DummySwitch extends Thread {
    private static final Logger log = LoggerFactory.getLogger(DummySwitch.class);

    public static final int HANDSHAKE_DEFAULT = 0;
    public static final int HANDSHAKE_NO_HELLO = 1;
    public static final int NO_HANDSHAKE = 2;
    public static final int MINIMUM_LENGTH = 8;

    private Socket socket;
    public Socket getSocket() {
		return socket;
	}
	private InputStream in;
    private OutputStream out;
    /* for OF message */
    private OFFactory factory = OFFactories.getFactory(OFVersion.OF_13);;
    private OFMessageReader<OFMessage> reader = factory.getReader();
    private OFMessage res;
    private int testHandShakeType;
    private static final long DEFAULT_XID = 0xeeeeeeeel;
    private static final DatapathId DEFAULT_DPID = DatapathId.of((long) 1);
    private long requestXid;
    private boolean handshaked = false;
    private boolean synack = false;
    private DatapathId dpid;
    private Configuration config = Configuration.getInstance();
    private Strategy strategy;
    public volatile boolean running = true;
    private boolean strategySend = false;
    private boolean strategyCompleted = false;
	private OFFlowAdd backupFlowAdd;
    private ConcurrentHashMap<OFFlowStatsEntry, OFFlowStatsEntry> flowTable = new ConcurrentHashMap<>();
    private OFState state = new OFState(false);

    public DummySwitch() {
    	this.dpid = DEFAULT_DPID;
    }
    public DummySwitch(Strategy strategy) {
        this.strategy = strategy;
        this.dpid = DEFAULT_DPID;
    }
    public DummySwitch(DatapathId dpid) {
        this.res = null;
        this.dpid = dpid;
    }

    
    @Override
    public void run() {
    	do {
    		try {
    			socket = new Socket(config.getControllerIP(), Integer.parseInt(config.getControllerPort()));
				socket.setReuseAddress(true);
			} catch (UnknownHostException e) {
				e.printStackTrace();
				break;
			} catch (SocketException e) {
				log.info(e.toString()+"\n"+Arrays.toString(e.getStackTrace()));
				continue;
			}catch (IOException e) {
				log.info(e.toString()+"\n"+Arrays.toString(e.getStackTrace()));
				continue;
			} 
    	}while(socket == null);
    	
    	try {
			in = socket.getInputStream();
			out = socket.getOutputStream();
		} catch (IOException e2) {
			e2.printStackTrace();
		}
    	
    	//发送hello报文
    	try {
			Thread.sleep(3);
		} catch (InterruptedException e2) {
			e2.printStackTrace();
		}
    	state.processSendHello();
    	
        byte[] recv;
        int readlen;

        try {
            while (running && !Thread.currentThread().isInterrupted()) {
                recv = new byte[2048];
                if ((readlen = in.read(recv, 0, recv.length)) != -1) {
                    if (readlen >= 8)
                        parseOFMsg(recv, readlen);
                } else
                    break; // end of connection
            }
        } catch (SocketException e) {
			log.info("[DummySwitch] catch SocketException: controller require connection reset");
		}catch (Exception e) {
            // if any error occurs
        	Thread.currentThread().interrupt();
            e.printStackTrace();
        } finally {
            try {
                if (in != null)
                    in.close();
                if (socket != null)
                    socket.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
    
    private boolean parseOFMsg(byte[] recv, int len) throws OFParseError {
        // for OpenFlow Message
        byte[] rawMsg = new byte[len];
        System.arraycopy(recv, 0, rawMsg, 0, len);
        ByteBuf bb = Unpooled.copiedBuffer(rawMsg);

        int totalLen = bb.readableBytes();
        int offset = bb.readerIndex();

        while (offset < totalLen) {
            bb.readerIndex(offset);

            byte version = bb.readByte();

            if (version >= 5) {
                log.info("[Channel Agent] OF Version >= 1.4 not supported");
                return false;
            }

            // version check
            if (version != factory.getVersion().getWireVersion()) {
                log.info("[Channel Agent] Received OF Version {} not matched with the dummy switch", version);
                return false;
            }

            bb.readByte();
            int length = U16.f(bb.readShort());
            bb.readerIndex(offset);

            if (length < MINIMUM_LENGTH)
                throw new OFParseError("Wrong length: Expected to be >= " + MINIMUM_LENGTH + ", was: " + length);

            OFMessage message = reader.readFrom(bb);
            state.processOFMsg(message);
        }

        bb.clear();
        return true;
    }
    
    public class OFState{
    	private boolean HandshakeComplete;
    	OFState(boolean handshakeComplete) {
			this.HandshakeComplete = handshakeComplete;
		}
    	
    	void processOFMsg(OFMessage m) {
    		System.out.println(m.toString());
    		switch(m.getType()) {
			case HELLO:
				processOFHello((OFHello)m);
				break;
			case FEATURES_REQUEST:
				processOFFeaturesReq((OFFeaturesRequest)m);
				break;
			case GET_CONFIG_REQUEST:
				processOFGetConfigRequest((OFGetConfigRequest)m);
				break;
			case BARRIER_REQUEST:
				processOFBarrierRequest((OFBarrierRequest)m);
				break;
			case STATS_REQUEST:
				processOFStatsRequest((OFStatsRequest)m);
				break;
			case EXPERIMENTER:
				processOFExperimenter((OFExperimenter)m);
				break;
			case ECHO_REQUEST:
				processOFEchoRequest((OFEchoRequest)m);
				break;
			case ECHO_REPLY:
				processOFEchoReply((OFEchoReply)m);
				break;
			case ERROR:
				processOFError((OFErrorMsg)m);
				break;
			case FLOW_MOD:
				processOFFlowMod((OFFlowMod)m);
				break;
			case ROLE_REQUEST:
				processOFRoleRequest((OFRoleRequest)m);
				break;
			case PACKET_OUT:
				processOFPacketOut((OFPacketOut)m);
				break;
			}
    	}

    	void processOFPacketOut(OFPacketOut m) {
    		log.info("Reveived a OFPacketOut.");
		}

		void processOFHello(OFHello m) {
			log.info("Reveived a OFHello.");
    	}
		public void processSendHello() {
	    	if(strategy != null && strategy.ofType == OFType.HELLO) {
	    		FuHello hello = new FuHello();
	    		hello.setField(strategy.field, strategy.actionValue);
	    		sendByteBufMsg(hello.writeTo());
	    		strategySend = true;
	    	}else {
	    		sendHello(0);
	    	}
	    }
		
		void processOFRoleRequest(OFRoleRequest m) {
			log.info("Reveived a OFRoleRequest.");
			if(strategy != null && strategy.ofType == OFType.ROLE_REPLY) {
				FuRoleReply roleReply = new FuRoleReply();
				roleReply.xid = m.getXid();
				roleReply.setField(strategy.field, strategy.actionValue);
        		sendByteBufMsg(roleReply.writeTo());
        		strategySend = true;
			}else {
				sendRoleRes(m.getXid());
			}
			log.info("send a roleReply msg to controller,strategySend = " + strategySend);
		}
    	
		void processOFFlowMod(OFFlowMod m) {
			log.info("Reveived a OFFlowMod.");
			OFFlowMod fm = (OFFlowMod) m;

            processFlowMod(fm);

            if (fm.getCommand() == OFFlowModCommand.ADD) {
                OFFlowAdd fa = (OFFlowAdd) fm;
                if (fa.getPriority() == 555) {
                    backupFlowAdd = fa;
                    log.info("[Channel Agent] catch un-flagged msg {}", fa.toString());
                }
            }
		}
    	
		void processOFError(OFErrorMsg m) {
			log.info("Reveived a OFErrorMsg.");
			printError(m);
		}
    	
		void processOFEchoReply(OFEchoReply m) {
    		log.info("Reveived a OFEchoReply.");
		}
    	
		void processOFEchoRequest(OFEchoRequest m) {
			log.info("Reveived a OFEchoRequest.");
    		if(strategy != null && strategy.ofType == OFType.ECHO_REPLY) {
				FuEchoReply echoReply = new FuEchoReply();
				echoReply.xid = m.getXid();
				echoReply.setField(strategy.field, strategy.actionValue);
        		sendByteBufMsg(echoReply.writeTo());
        		strategySend = true;
			}else {
				sendEchoReply(m.getXid());
//				byte[] msg = Utils.hexStringToByteArray(DummyOF13.ECHO_REPLY);
//        		sendRawMsg(msg);
			}
    		log.info("send a echoReply msg to controller,strategySend = " + strategySend);
		}
    	
		void processOFExperimenter(OFExperimenter m) {
			log.info("Reveived a OFExperimenter.");
			sendExperimenter(m.getXid());
			log.info("send a ExperimenterReply msg to controller,strategySend = " + strategySend);
		}
    	
		void processOFStatsRequest(OFStatsRequest m) {
			log.info("Reveived a OFStatsRequest.");
			sendStatReply(m, m.getXid());
//            handshaked = true;
			log.info("send a StatsReply msg to controller,strategySend = " + strategySend);
		}
    	
		void processOFBarrierRequest(OFBarrierRequest m) {
			log.info("Reveived a OFBarrierRequest.");
			if(strategy != null && strategy.ofType == OFType.BARRIER_REPLY) {
				FuBarrierReply barrierReply = new FuBarrierReply();
				barrierReply.xid = m.getXid();
				barrierReply.setField(strategy.field, strategy.actionValue);
        		sendByteBufMsg(barrierReply.writeTo());
        		strategySend = true;
			}else {
				sendBarrierRes(m.getXid());
			}
			log.info("send a BarrierReply msg to controller,strategySend = " + strategySend);
		}
    	
		void processOFGetConfigRequest(OFGetConfigRequest m) {
			log.info("Reveived a OFGetConfigRequest.");
			if(strategy != null && strategy.ofType == OFType.GET_CONFIG_REPLY) {
				FuGetConfigReply getConfigReply = new FuGetConfigReply();
				getConfigReply.xid = m.getXid();
				getConfigReply.setField(strategy.field, strategy.actionValue);
        		sendByteBufMsg(getConfigReply.writeTo());
        		strategySend = true;
			}else {
				sendGetConfigReply(m.getXid());
			}
			log.info("send a ConfigReply msg to controller,strategySend = " + strategySend);
		}
		
		void processOFFeaturesReq(OFFeaturesRequest m) {
    		log.info("receive OFFeaturesRequest msg");
    		//send FeatureReply
    		if(strategy != null && strategy.ofType == OFType.FEATURES_REPLY) {
    			FuFeaturesReply featuresReply = new FuFeaturesReply();
    			featuresReply.xid = m.getXid();
    			featuresReply.datapathId = dpid.getLong();
    			featuresReply.setField(strategy.field, strategy.actionValue);
        		sendByteBufMsg(featuresReply.writeTo());
        		strategySend = true;
        	}else {
//        		byte[] msg = Utils.hexStringToByteArray(DummyOF13.FEATURE_REPLY);
//        		sendRawMsg(msg);
//        		ByteBuf bb =  Unpooled.copiedBuffer(msg);
//        		OFMessage msgMessage = null;
//        		try {
//        			msgMessage = OFFactories.getFactory(OFVersion.OF_13).getReader().readFrom(bb);
//        		} catch (OFParseError e1) {
//        			e1.printStackTrace();
//        		}
//        		OFFeaturesReply reply = (OFFeaturesReply)msgMessage;
        		
        		try {
					sendFeatureReply(m.getXid());
				} catch (OFParseError e) {
					e.printStackTrace();
				}
        	}
//    		log.info("send a featuresReply msg to controller,strategySend = " + strategySend);
    	}
    }
    
    public void printError(OFMessage msg) {
        System.out.println(msg.toString());
    }

    public void sendMsg(OFMessage msg, int len) {
        ByteBuf buf;

        if (len == -1) {
            buf = PooledByteBufAllocator.DEFAULT.directBuffer(1024);
        } else {
            buf = PooledByteBufAllocator.DEFAULT.directBuffer(len);
        }

        msg.writeTo(buf);

        int length = buf.readableBytes();
        byte[] bytes = new byte[length];
        buf.getBytes(buf.readerIndex(), bytes);

        try {
            this.out.write(bytes, 0, length);
        } catch (IOException e) {
            e.printStackTrace();
        }

        buf.clear();
        buf.release();
    }

    public void setReqXid(long xid) {
        requestXid = xid;
    }

    public OFMessage getResponse() {
        return res;
    }

    public void sendRawMsg(byte[] msg) {
        try {
            this.out.write(msg, 0, msg.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public void sendByteBufMsg(ByteBuf buf) {
    	int length = buf.readableBytes();
        byte[] bytes = new byte[length];
        buf.getBytes(buf.readerIndex(), bytes);

        try {
            this.out.write(bytes, 0, length);
        } catch (IOException e) {
            e.printStackTrace();
        }

        buf.clear();
        buf.release();
    }

    public OFFlowAdd getBackupFlowAdd() {
        return backupFlowAdd;
    }


    public void sendError() throws OFParseError {
        long r_xid = 0xeeeeeeeel;

        OFErrorMsgs msg = factory.errorMsgs();
        OFFeaturesReply.Builder frb = factory.buildFeaturesReply();
        OFFeaturesReply fr = frb.build();

        return;
    }

    public ByteBuf sendFlowRemoved() throws OFParseError {
        OFFactory factory = OFFactories.getFactory(OFVersion.OF_10);
        long r_xid = 0xeeeeeeeel;

        OFFlowRemoved.Builder fab = factory.buildFlowRemoved();
        fab.setXid(r_xid);
        OFFlowRemoved hello = fab.build();

        ByteBuf buf = null;
        buf = PooledByteBufAllocator.DEFAULT.directBuffer(88);
        hello.writeTo(buf);

        byte[] bytes;
        int length = buf.readableBytes();
        bytes = new byte[length];
        buf.getBytes(buf.readerIndex(), bytes);

        try {
            this.out.write(bytes, 0, length);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return buf;
    }

    public ByteBuf sendPortStatus() throws OFParseError {
        OFFactory factory = OFFactories.getFactory(OFVersion.OF_10);
        long r_xid = 0xeeeeeeeel;

        OFPortStatus.Builder fab = factory.buildPortStatus();
        fab.setXid(r_xid);
        OFPortStatus hello = fab.build();

        ByteBuf buf = null;
        buf = PooledByteBufAllocator.DEFAULT.directBuffer(64);
        hello.writeTo(buf);

        byte[] bytes;
        int length = buf.readableBytes();
        bytes = new byte[length];
        buf.getBytes(buf.readerIndex(), bytes);

        try {
            this.out.write(bytes, 0, length);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return buf;
    }

    public boolean getHandshaked() {
        return this.handshaked;
    }

    /* OF HandShake */
    public void sendHello(long xid){
        if (factory.getVersion() == OFVersion.OF_13) {
		    byte[] msg = Utils.hexStringToByteArray("0400000800000001");
		    sendRawMsg(msg);
		    log.info("send a of13 hello packet to controller.");
		    return;
		}

		OFHello.Builder builder = factory.buildHello();
//		builder.setXid(0xb0);
		builder.setXid(0x1);

		sendMsg(builder.build(), -1);
		log.info("send a of10 hello packet to controller.");
    }

    public void sendFeatureReply(long xid) throws OFParseError {
        OFFeaturesReply.Builder frb = factory.buildFeaturesReply();
        frb.setDatapathId(this.dpid);
        frb.setNBuffers((long) 0);//256
        frb.setNTables((short) 254);
        frb.setAuxiliaryId(OFAuxId.of((short) 0));
        frb.setReserved((long) 0);
        frb.setXid(xid);
        HashSet<OFCapabilities> ofCapabilities = new HashSet<OFCapabilities>();
        ofCapabilities.add(OFCapabilities.FLOW_STATS);
        ofCapabilities.add(OFCapabilities.TABLE_STATS);
        ofCapabilities.add(OFCapabilities.PORT_STATS);
        ofCapabilities.add(OFCapabilities.GROUP_STATS);
        ofCapabilities.add(OFCapabilities.QUEUE_STATS);
        frb.setCapabilities(ofCapabilities);
        OFFeaturesReply msg = frb.build();
        log.info("send a FeatureReply: "+msg.toString());
        sendMsg(msg, -1);
    }

    public void sendGetConfigReply(long xid) {
        byte[] msg;

        if (factory.getVersion() == OFVersion.OF_13) {
            msg = Utils.hexStringToByteArray(DummyOF13.GET_CONFIG_REPLY);
        } else {
            msg = Utils.hexStringToByteArray(DummyOF10.GET_CONFIG_REPLY);
        }

        byte[] xidbytes = Longs.toByteArray(xid);
        System.arraycopy(xidbytes, 4, msg, 4, 4);

        sendRawMsg(msg);
    }

    public void sendStatReply(OFMessage input, long xid) {
        byte[] msg;
        OFStatsReply replyMsg = null;

        if (factory.getVersion() == OFVersion.OF_13) {
            OFStatsRequest req = (OFStatsRequest) input;
            switch (req.getStatsType()) {
                case PORT_DESC:
                    msg = Utils.hexStringToByteArray(DummyOF13.MULTIPART_PORT_DESC);
                    break;
                case DESC:
                    msg = Utils.hexStringToByteArray(DummyOF13.MULTIPART_DESC);
                    break;
                case METER:
                    msg = Utils.hexStringToByteArray(DummyOF13.MULTIPART_METER);
                    break;
                case PORT:
                    msg = Utils.hexStringToByteArray(DummyOF13.MULTIPART_PORT_STATS);
                    break;
                case FLOW:
                    synchronized (this) {
                        replyMsg = factory.buildFlowStatsReply()
                                .setXid(input.getXid())
                                .setEntries(new ArrayList<>(flowTable.values()))
                                .build();
                        sendMsg(replyMsg, -1);
                    }
                    return;
                case METER_FEATURES:
                    replyMsg = factory
                            .buildMeterFeaturesStatsReply()
                            .setXid(input.getXid())
                            .setFeatures(factory.buildMeterFeatures().build())
                            .build();
                    sendMsg(replyMsg, -1);
                    return;
                case TABLE_FEATURES:

                    List<OFInstructionId> ofInstructionIds = Arrays.asList(
                            factory.instructionIds().applyActions(), factory.instructionIds().clearActions()
                    );
                    OFTableFeatureProp ofTableFeaturePropInstructions = factory.buildTableFeaturePropInstructions()
                            .setInstructionIds(ofInstructionIds)
                            .build();

                    List<OFActionId> ofActionsIds = Arrays.asList(
                            factory.actionIds().setField(), factory.actionIds().setNwTtl()
                    );
                    OFTableFeatureProp ofTableFeaturePropApplyActionsMiss = factory.buildTableFeaturePropApplyActionsMiss()
                            .setActionIds(ofActionsIds)
                            .build();

                    OFTableFeatures ofTableFeatures = factory.buildTableFeatures()
                            .setMetadataMatch(U64.NO_MASK)
                            .setMetadataWrite(U64.NO_MASK)
                            .setMaxEntries(1000000)
                            .setTableId(TableId.ZERO)
                            .setProperties(Arrays.asList(ofTableFeaturePropInstructions, ofTableFeaturePropApplyActionsMiss))
                            .build();

                    replyMsg = factory
                            .buildTableFeaturesStatsReply()
                            .setXid(input.getXid())
                            .setEntries(Arrays.asList(factory.buildTableFeatures().build()))
                            .setEntries(Arrays.asList(ofTableFeatures))
                            .build();
                    sendMsg(replyMsg, -1);

                    return;
                /* case AGGREGATE:
                    break;
                case TABLE:
                    break;
                case QUEUE:
                    break;
                case EXPERIMENTER:
                    break;
                case GROUP:
                    break;
                case GROUP_DESC:
                    break;
                case GROUP_FEATURES:
                    break;
                case METER_CONFIG:
                    break;
                case TABLE_DESC:
                    break;
                case QUEUE_DESC:
                    break;
                case FLOW_MONITOR:
                    break; */
                default:
                    return;
            }
        } else {
            msg = Utils.hexStringToByteArray(DummyOF10.STATS_REPLY);
        }

        byte[] xidbytes = Longs.toByteArray(xid);
        System.arraycopy(xidbytes, 4, msg, 4, 4);

        sendRawMsg(msg);
    }

    private void sendRoleRes(long xid) {
        OFRoleReply.Builder builder = factory.buildRoleReply();
        builder.setXid(xid);
        builder.setGenerationId(U64.of(0));
        builder.setRole(OFControllerRole.ROLE_MASTER);

        OFRoleReply msg = builder.build();
        sendMsg(msg, -1);
    }

    private void sendBarrierRes(long xid) {
        OFBarrierReply.Builder builder = factory.buildBarrierReply();
        builder.setXid(xid);
        OFBarrierReply msg = builder.build();
        sendMsg(msg, 8);
    }

    private void sendEchoReply(long xid) {
        OFEchoReply.Builder builder = factory.buildEchoReply();
        builder.setXid(xid);
        OFEchoReply msg = builder.build();
        sendMsg(msg, 8);
    }

    private void sendExperimenter(long xid) {
        byte[] msg = DummyOF10.hexStringToByteArray(DummyOF10.experimenter);
        byte[] xidbytes = Longs.toByteArray(xid);
        System.arraycopy(xidbytes, 4, msg, 4, 4);

        if (factory.getVersion() == OFVersion.OF_13)
            msg[0] = 0x04;

        sendRawMsg(msg);
    }

    //add flow stats entries
    private synchronized void processFlowMod(OFFlowMod msg) {

        ArrayList<OFInstruction> newInst = new ArrayList<>();
        List<OFInstruction> instructions = msg.getInstructions();
        for (OFInstruction inst : instructions) {
            if (inst.getType() == OFInstructionType.APPLY_ACTIONS) {
                newInst.add(inst);
            }
        }

        OFFlowStatsEntry ofFlowStatsEntry = factory.buildFlowStatsEntry()
                .setInstructions(newInst)
                .setCookie(msg.getCookie())
                .setFlags(msg.getFlags())
                .setPriority(msg.getPriority())
                .setTableId(msg.getTableId())
                .setMatch(msg.getMatch())
                .build();

        flowTable.put(ofFlowStatsEntry, ofFlowStatsEntry);
    }

}
